/*
    Priscilla - A library for enocean network access
    Copyright (C) 2012-2013 B. Aigner / R. Wagner

    This program is free software; you can redistribute it and/or 
    modify it under the terms of the GNU General Public License 
    as published by the Free Software Foundation; either version 2 
    of the License, or at your option any later version. 
 
    This program is distributed in the hope that it will be useful, 
    but WITHOUT ANY WARRANTY; without even the implied warranty of 
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
    GNU General Public License for more details. 
 
    You should have received a copy of the GNU General Public License 
    along with this program; if not, write to the Free Software 
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. 
 
    Linking this library statically or dynamically with other modules is 
    making a combined work based on this library. Thus, the terms and 
    conditions of the GNU General Public License cover the whole 
    combination. 
 
    As a special exception, the copyright holders of this library give you 
    permission to link this library with independent modules to produce an 
    executable, regardless of the license terms of these independent 
    modules, and to copy and distribute the resulting executable under terms 
    of your choice, provided that you also meet, for each linked independent 
    module, the terms and conditions of the license of that module. An 
    independent module is a module which is not derived from or based on 
    this library. If you modify this library, you may extend this exception 
    to your version of the library, but you are not obligated to do so. If 
    you do not wish to do so, delete this exception statement from your 
    version. 
*/

package at.technikum_wien.embsys.aat.PriscillaCore.xml;

import java.io.File;
import org.apache.log4j.Logger;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;


import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import at.technikum_wien.embsys.aat.PriscillaCore.enums.ErrorCodeLRN;
import at.technikum_wien.embsys.aat.PriscillaCore.enums.TeachInOptions;
import at.technikum_wien.embsys.aat.PriscillaCore.link.EnOceanLinkImpl;
import at.technikum_wien.embsys.aat.PriscillaCore.telegram.Device;
import at.technikum_wien.embsys.aat.PriscillaCore.telegram.Telegram;


/**
 * XML device file access for determining FUNC and TYPE by a sensors ID
 * 
 * This class gives access to the devices.xml file,
 * which contains the learned sensors. By calling getList, a list with
 * every known device will be returned. The list contains elements of type Device.
 * For adding new devices, the methods createNewXMLEntry and recreateXML are used.
 * 
 * @author Benjamin Aigner
 * @version 1.0
 * 
 */
public class DeviceListXML {
	
	public static final String FILENAME_DEFAULT = "devices.xml";
	
	private static final String FILENAME_BACKUP = "devices_backup.xml";
	
	private File devicesFile = new File(FILENAME_DEFAULT);
	
	private DocumentBuilderFactory dbf;
	
	private static final Logger log = Logger.getLogger(EnOceanLinkImpl.class);
	
	/**
	 * Manipulating teach in options:
	 * Registered device can be updated with new 
	 * TYPE/FUNC, otherwise the teach in method
	 * returns an error if the sensor is known
	 */
	private TeachInOptions options;

	/**
	 * Void ctor, using one time teach-in (an error occurs, if
	 * an already known sensor should be teached-in).
	 */
	public DeviceListXML(File devicesFile) {
		this.options = TeachInOptions.ONE_TIME; //only one time teach-in
		
		if (devicesFile != null) {
			this.devicesFile = devicesFile;
		}
	}

	/**
	 * Ctor with a given learn-in option
	 * 
	 * If this constructor is used, the program will use the given learn-in mode.
	 * 
	 * @param options Define the teach in options
	 */
	public DeviceListXML (File devicesFile, TeachInOptions options) {
		if(options != null) {
			this.options = options;	//save teach in options
		} else {
			this.options = TeachInOptions.ONE_TIME; //default: one-time
		}
		
		if (devicesFile != null) {
			this.devicesFile = devicesFile;
		}
	}

	/**
	 * Get list of devices, extracted from XML file
	 * 
	 * This method reads out the XML device list and returns the known
	 * devices from it by an list
	 * 
	 * @return the actual device list from the XML file devices.xml
	 * 
	 */
	public List<Device> getList() {
		Document dom;
		dbf = DocumentBuilderFactory.newInstance();
		NodeList nl;
		List<Device> list = new ArrayList<Device>();
		try {

			// Using factory get an instance of document builder
			DocumentBuilder db = dbf.newDocumentBuilder();

			// parse using builder to get DOM representation of the XML file
			dom = db.parse(devicesFile);

			Element docEle = dom.getDocumentElement();
			nl = docEle.getElementsByTagName("EnoceanDevice"); //Tag for one enocean device

			if (nl != null && nl.getLength() > 0) {
				for (int i = 0; i < nl.getLength(); i++) {

					// get the enocean component element
					Element el = (Element) nl.item(i);

					Device d = new Device(); //create a new device instance
					
					//parse data into the device instance
					d.func = Integer.parseInt(getTextValue(el, "FUNC"),16);
					d.type = Integer.parseInt(getTextValue(el, "TYPE"),16);
					d.id0 = Integer.parseInt(getTextValue(el, "ID0"),16);
					d.id1 = Integer.parseInt(getTextValue(el, "ID1"),16);
					d.id2 = Integer.parseInt(getTextValue(el, "ID2"),16);
					d.id3 = Integer.parseInt(getTextValue(el, "ID3"),16);

					// add it to list
					list.add(d);
				}
			}

		} catch (ParserConfigurationException pce) {
			log.error("corrupted XML file, backing up old one and creating new one");
			recreateXML(list);
		} catch (SAXParseException spe) {	
			log.error("corrupted XML file, backing up old one and creating new one");
			try {
				copy(devicesFile.getAbsolutePath(), FILENAME_BACKUP);
			} catch (IOException e) {
				log.error("error while backup the old XML file");
			}
			recreateXML(list); //recreate the XML
		} catch (SAXException se) {
			log.error("corrupted XML file, backing up old one and creating new one");
			recreateXML(list);
		} catch (IOException ioe) {
			log.info("no XML device file found, creating a new one");
			recreateXML(list);
		}
		return list;
	}

	/**
	 * Add a new device
	 * 
	 * This function adds a device (given by the teach-in telegram)
	 * to the list of known devices and recreates the XML file with the new list
	 * 
	 * @param list	the actual device list
	 * @param telegram The new teach-in telegram
	 * @return Error code from ErrorCodeLRN
	 * @throws ParserConfigurationException 
	 * @throws SAXException
	 * @throws IOException
	 */
	public ErrorCodeLRN createNewXMLEntry(List<Device> list, Telegram telegram)
			throws ParserConfigurationException, SAXException, IOException {
		int type;
		int func;
		Device newdev = new Device();
		Device olddev = new Device();
		boolean known = false;

		if (telegram.getORG() != 0x07){			//no teach-in telegrams possible if the ORG is not 0x07
			log.info("no ORG07 telegram, no teach in possible");
			return ErrorCodeLRN.NO_ORG_07_TELEGRAM;
		}
		
		if(((byte)telegram.getData0() & 0x80) == 0) { //teach-in variant 1 --> TYPE and FUNC must be provided
			log.info("no type/func provided, no teach in possible");
			return ErrorCodeLRN.UNABLE_TO_LEARN;
		}

		//extract FUNC and TYPE from the teach-in telegram
		func = (((byte) telegram.getData3()) & 0xFC) >> 2;
		type = (((byte) telegram.getData3() & 0x03) << 5)
				+ (((byte) telegram.getData2() & 0xF8) >> 3);

		if (func == 0) { //no FUNC can be extracted -> return error
			log.info("no func, no teach in possible");
			return ErrorCodeLRN.FUNC_0_TELEGRAM;
		}

		//save FUNC TYPE and device ID into new device 
		newdev.func = func;
		newdev.type = type;
		newdev.id0 = telegram.getSensorID0();
		newdev.id1 = telegram.getSensorID1();
		newdev.id2 = telegram.getSensorID2();
		newdev.id3 = telegram.getSensorID3();

		//search the list for an already existing entry
		for (int i = 0; i < list.size(); i++) {
			if (telegram.getSensorID0() == list.get(i).id0) {
				if (telegram.getSensorID1() == list.get(i).id1) {
					if (telegram.getSensorID2() == list.get(i).id2) {
						if (telegram.getSensorID3() == list.get(i).id3) {
							//if an entry was found, mark the new device as known
							known = true;
							//if the instance is configured for one-time teach-in,
							//return error
							if(this.options == TeachInOptions.ONE_TIME) {
								log.info("known device, no teach in");
								return ErrorCodeLRN.KNOWN_DEVICE;
							}
							//otherwise overwrite the entry
							if(this.options == TeachInOptions.OVERWRITE) {
								olddev.func = list.get(i).func;
								olddev.type = list.get(i).type;
								olddev.id0 = list.get(i).id0;
								olddev.id1 = list.get(i).id1;
								olddev.id2 = list.get(i).id2;
								olddev.id3 = list.get(i).id3;
								if(newdev.equals(olddev)) return ErrorCodeLRN.SUCCESS;
								list.set(i, newdev);
							}
						}
					}
				}
			}
		}

		if(known == false) list.add(newdev); //if the device is not known, add it to the list
		log.info("added a new device to XML device file: " + telegram.getDeviceID() + ", type: " + newdev.type + ", func: " + newdev.func);
		return recreateXML(list); //recreate the XML file with the new entry
	}

	/**
	 * Get the text value of an element.
	 * 
	 * @param ele Given Element
	 * @param tagName Search for this tag name
	 * @return String representation of the element value
	 */
	private String getTextValue(Element ele, String tagName) {
		String textVal = null;
		NodeList nl = ele.getElementsByTagName(tagName);
		if (nl != null && nl.getLength() > 0) {
			Element el = (Element) nl.item(0);
			textVal = el.getFirstChild().getNodeValue();
		}

		return textVal;
	}
	
	/**
	 * This method recreates the XML file and fills it with the given devices
	 * 
	 * @param list Actual device list
	 * @return Error code
	 */
	public ErrorCodeLRN recreateXML(List<Device> list) {
		Document dom;

		//node instances for each device node element
		Node oldnode;
		Node oldtype;
		Node oldfunc;
		Node oldid0;
		Node oldid1;
		Node oldid2;
		Node oldid3;

		try {
			// Using factory get an instance of document builder
			DocumentBuilder db = dbf.newDocumentBuilder();
			dom = db.newDocument();

			Node rootnode = dom.createElement("EnoceanDevices"); //create root node
			dom.appendChild(rootnode);

			//iterate over the list
			for(int i = 0; i < list.size(); i++) {
				oldnode = dom.createElement("EnoceanDevice"); //create device node

				oldfunc = dom.createElement("FUNC");
				oldfunc.setTextContent(Integer.toHexString(list.get(i).func)); //create FUNC element

				oldtype = dom.createElement("TYPE");
				oldtype.setTextContent(Integer.toHexString(list.get(i).type)); //create TYPE element

				oldid0 = dom.createElement("ID0");
				oldid0.setTextContent(Integer.toHexString(list.get(i).id0)); //create device ID0 element

				oldid1 = dom.createElement("ID1");
				oldid1.setTextContent(Integer.toHexString(list.get(i).id1)); //create device ID1 element

				oldid2 = dom.createElement("ID2");
				oldid2.setTextContent(Integer.toHexString(list.get(i).id2)); //create device ID2 element

				oldid3 = dom.createElement("ID3");
				oldid3.setTextContent(Integer.toHexString(list.get(i).id3)); //create device ID3 element

				//append all elements to the device node
				oldnode.appendChild(oldfunc);
				oldnode.appendChild(oldtype);
				oldnode.appendChild(oldid0);
				oldnode.appendChild(oldid1);
				oldnode.appendChild(oldid2);
				oldnode.appendChild(oldid3);

				//append the device node to the root node
				rootnode.appendChild(oldnode);
			}

			//create a new transformer
			TransformerFactory transformerFactory = TransformerFactory
					.newInstance();
			Transformer transformer;
			transformer = transformerFactory.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");

			//create a DOM source
			DOMSource source = new DOMSource(dom);
			StreamResult result = new StreamResult(devicesFile); //open the file
			transformer.transform(source, result); //save the DOM representation into the XML file
		} catch (DOMException e) {
			log.error("XML device database - recreation failed: DOMException");
			return ErrorCodeLRN.UNKNOWN_ERROR;
		} catch (TransformerConfigurationException e) {
			log.error("XML device database - recreation failed: TransformerConfigException");
			return ErrorCodeLRN.UNKNOWN_ERROR;
		} catch (IllegalArgumentException e) {
			log.error("XML device database - recreation failed: ArgumentException");
			return ErrorCodeLRN.UNKNOWN_ERROR;
		} catch (ParserConfigurationException e) {
			log.error("XML device database - recreation failed: ParserConfigException");
			return ErrorCodeLRN.UNKNOWN_ERROR;
		} catch (TransformerFactoryConfigurationError e) {
			log.error("XML device database - recreation failed: TransformerFactoryError");
			return ErrorCodeLRN.XML_TransformerException;
		} catch (TransformerException e) {
			log.error("XML device database - recreation failed: TransformerException");
			return ErrorCodeLRN.XML_TransformerException;
		}
		return ErrorCodeLRN.SUCCESS; //return success
	}


	/**
	 * Copy method for saving a file to a new position
	 * 
	 * @param fromFileName Source file name
	 * @param toFileName Destination file name
	 * @throws IOException
	 * 
	 */
	private static void copy(String fromFileName, String toFileName)
			throws IOException {
		File fromFile = new File(fromFileName);
		File toFile = new File(toFileName);

		if (!fromFile.exists())
			throw new IOException("FileCopy: " + "no such source file: "
					+ fromFileName);
		if (!fromFile.isFile())
			throw new IOException("FileCopy: " + "can't copy directory: "
					+ fromFileName);
		if (!fromFile.canRead())
			throw new IOException("FileCopy: " + "source file is unreadable: "
					+ fromFileName);

		if (toFile.isDirectory())
			toFile = new File(toFile, fromFile.getName());

		if (toFile.exists()) {
			if (!toFile.canWrite())
				throw new IOException("FileCopy: "
						+ "destination file is unwriteable: " + toFileName);
		} else {
			String parent = toFile.getParent();
			if (parent == null)
				parent = System.getProperty("user.dir");
			File dir = new File(parent);
			if (!dir.exists())
				throw new IOException("FileCopy: "
						+ "destination directory doesn't exist: " + parent);
			if (dir.isFile())
				throw new IOException("FileCopy: "
						+ "destination is not a directory: " + parent);
			if (!dir.canWrite())
				throw new IOException("FileCopy: "
						+ "destination directory is unwriteable: " + parent);
		}

		FileInputStream from = null;
		FileOutputStream to = null;
		try {
			from = new FileInputStream(fromFile);
			to = new FileOutputStream(toFile);
			byte[] buffer = new byte[4096];
			int bytesRead;

			while ((bytesRead = from.read(buffer)) != -1)
				to.write(buffer, 0, bytesRead); // write
		} finally {
			if (from != null)
				try {
					from.close();
				} catch (IOException e) {
					;
				}
			if (to != null)
				try {
					to.close();
				} catch (IOException e) {
					;
				}
		}
	}
}
